# coding: utf-8
from jinja2 import Template
import sys
import os
import platform
import json
import errno
import subprocess
import uuid
import argparse
import traceback
import shutil
import stat

global_config = {}
project_config = {}

version_info = """
smake Dependency Configuration Tool
version: 1.0.1
author:By SkyFire
"""


def error_output(*args):
    msg = list(map(lambda x: str(x), args))
    print("ERROR-->" + " ".join(msg))
    traceback.print_exc()


def msg_output(*args):
    msg = list(map(lambda x: str(x), args))
    print("MSG  -->" + " ".join(msg))


def warn_output(*args):
    msg = list(map(lambda x: str(x), args))
    print("WARN -->" + " ".join(msg))


def succ_output(*args):
    msg = list(map(lambda x: str(x), args))
    print("SUCC -->" + " ".join(msg))


def del_dir_tree(path):
    if os.path.isfile(path):
        try:
            os.chmod( path,  stat.S_IWRITE | stat.S_IREAD )
            os.remove(path)
            msg_output("Delete -->", path)
        except Exception as e:
            error_output("Delete file error", path, e)
    elif os.path.isdir(path):
        for item in os.listdir(path):
            itempath = os.path.join(path, item)
            del_dir_tree(itempath)
        try:
            os.chmod( path, stat.S_IWRITE | stat.S_IREAD )
            os.rmdir(path)
            msg_output("Delete dir -->", path)
        except Exception as e:
            error_output("Delete dir error", path, e)


def ask_abort(error_question="A critical error has occurred, whether to continue?"):
    while True:
        answer = input(error_question + " (Y/n) ")
        if answer == "y" or answer == "Y":
            return
        elif answer == "n" or answer == "N":
            exit(-1)

def ask(error_question:str):
    while True:
        answer = input(error_question + " (Y/n) ")
        if answer == "y" or answer == "Y":
            return True
        elif answer == "n" or answer == "N":
            return False


def load_default_config():
    global project_config
    global global_config
    if platform.system() == "Windows":
        shell = "cmd /c"
        home_dir = os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"])
    else:
        shell = "/bin/bash"
        home_dir = os.path.expanduser("~")

    global_config = {
        "shell": shell,
        "home_dir": home_dir,
        "download_path": home_dir,
        "git_bin": "git",
        "svn_bin": "svn",
        "work_dir": os.path.abspath(os.curdir),
        "curr_script": os.path.abspath(sys.argv[0]),
        "old_env": os.environ,
        "system_type": platform.system(),
        "path_stack": [os.path.abspath(os.curdir)],
        "package_install_mark_dir": os.path.join(home_dir, "smake", "smakeInstalledPackage"),
        "pkg_list_config": os.path.join(home_dir,"smake","pkgListConfig"),
        "pkg_source":[],
        "env": {

        }
    }
    try:
        global_config_path = os.path.join(home_dir, ".smake.config.json")
        with open(global_config_path, "r") as fp:
            config = json.loads(fp.read())
            global_config.update(config)
        os.environ.update(global_config["env"])

    except OSError as err:
        if err.errno == errno.ENOENT:
            warn_output(global_config_path, "not found, will use default configuration")


def make_dirs(dirs: str):
    try:
        os.makedirs(dirs)
    except OSError as err:
        if err.errno == errno.EEXIST and os.path.isdir(dirs):
            pass
        else:
            error_output(err)
            exit(-1)


def make_install_mark_dir():
    global project_config
    global global_config
    make_dirs(global_config["package_install_mark_dir"])


def to_json_encode_str(value):
    if isinstance(value, str):
        return value.replace("\\", "\\\\").replace("\"", "\\\"") \
            .replace("\/", "\\/").replace("\b", "\\b").replace("\f", "\\f").replace(
            "\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
    return value


def load_project_config(config_file = "smake.json"):
    global project_config
    global global_config
    with open(config_file, "r") as fp:
        project_config_content = fp.read()
        tmp_render_obj = {}
        for key in os.environ.keys():
            tmp_render_obj[key] = to_json_encode_str(os.environ[key])
        for key in project_config.keys():
            tmp_render_obj[key] = to_json_encode_str(project_config[key])
        rendered_config = Template(project_config_content).render(**tmp_render_obj)
        project_config_obj = json.loads(rendered_config)
        project_config.update(project_config_obj)
        if "env" in project_config:
            os.environ.update(project_config["env"])


def run_cmd(cmd: str):
    global project_config
    global global_config
    msg_output("Run cmd:",cmd)
    while True:
        try:
            subprocess.check_call(cmd, shell=True)
            return
        except Exception as err:
            error_output(err)
            ask_abort(cmd + " Failure to execute, Retry?")


def change_dir(dir_path: str):
    global project_config
    global global_config
    global_config["path_stack"].append(os.path.abspath(os.curdir))
    msg_output("Change to", dir_path)
    os.chdir(os.path.abspath(dir_path))


def change_back_dir():
    global project_config
    global global_config
    new_path = global_config["path_stack"].pop()
    msg_output("Change to", new_path)
    os.chdir(new_path)


def load_pkg_export_env(mark_path: str):
    global project_config
    global global_config
    with open(mark_path) as fp:
        pkg_obj = json.loads(fp.read())
        if "install" in pkg_obj:
            if "export" in pkg_obj["install"]:
                os.environ.update(pkg_obj["install"]["export"])
                project_config["install"]["export"].update(pkg_obj["install"]["export"])
            system_type = global_config["system_type"]
            if system_type in pkg_obj["install"]:
                if "export" in pkg_obj["install"][system_type]:
                    os.environ.update(pkg_obj["install"][system_type]["export"])
                    if system_type not in project_config["install"]:
                        project_config["install"][system_type] = {}
                    if "export" not in project_config["install"][system_type]:
                        project_config["install"][system_type]["export"]= {}
                    project_config["install"][system_type]["export"].update(pkg_obj["install"][system_type]["export"])


def find_support_pkg(name:str,version:str):
    global global_config
    global project_config
    for pkg in global_config["pkg_source"]:
        if pkg["name"] == name and pkg["version"] == version:
            return pkg
    return None


def download_install_depend_item(dep):
    global project_config
    global global_config
    pkg_name = dep["name"] + dep["version"]
    mark_path = os.path.join(global_config["package_install_mark_dir"], pkg_name)
    if os.path.exists(mark_path):
        succ_output(pkg_name, "has already installed")
        load_pkg_export_env(mark_path)
        return
    src_path = os.path.join(global_config["download_path"], pkg_name)
    if os.path.exists(src_path):
        msg_output("Remove path",src_path)
        #shutil.rmtree(src_path, ignore_errors=True)
        del_dir_tree(src_path)
    get_src_cmd = None
    script_path = None
    if "git" in dep:
        get_src_cmd = global_config["git_bin"] + " clone " + dep["git"] + " \"" + src_path + "\""
    elif "svn" in dep:
        get_src_cmd = global_config["svn_bin"] + " checkout " + dep["svn"] + " \"" + src_path + "\""
    else:
        result = find_support_pkg(dep["name"] ,dep["version"])
        if result is None:
            ask_abort(pkg_name + " -> Git path or SVN path is not specified, Continue?")
            return
        else:
            if "git" in result:
                get_src_cmd = global_config["git_bin"] + " clone " + result["git"] + " \"" + src_path + "\""
            elif "svn" in result:
                get_src_cmd = global_config["svn_bin"] + " checkout " + dep["svn"] + " \"" + src_path + "\""
            else:
                ask_abort(pkg_name + " -> Git path or SVN path is not specified, Continue?")
                return
            if "script" in result:
                script_path = os.path.join(global_config["pkg_list_config"], "script", result["script"])
            else:
                ask_abort(pkg_name + " -> Third-party library configuration scripts not specified, Continue?")
    run_cmd(get_src_cmd)
    change_dir(src_path)
    config_file = os.path.join(src_path, "smake.json")
    if not os.path.exists(config_file):
        if script_path is not None:
            shutil.copy(script_path,config_file)
    run_cmd("python \"" + global_config["curr_script"] + "\"")
    change_back_dir()
    load_pkg_export_env(mark_path)


def download_and_build_depend():
    global project_config
    global global_config
    if "install" not in project_config:
        project_config["install"] = {}
    if "export" not in project_config["install"]:
        project_config["install"]["export"] = {}
    if "build" not in project_config:
        return
    if "depend" in project_config["build"]:
        for dep in project_config["build"]["depend"]:
            download_install_depend_item(dep)
    system_type = global_config["system_type"]
    if system_type in project_config["build"]:
        if "depend" in project_config["build"][system_type]:
            for dep in project_config["build"][system_type]["depend"]:
                download_install_depend_item(dep)
    load_project_config()


def run_cmd_list(cmd_list: list):
    global project_config
    global global_config
    file_content = "\n".join(cmd_list)
    if global_config["system_type"] == "Windows":
        file_content = "@echo off\n" + file_content
    tmp_file = str(uuid.uuid1()) + ".bat"
    with open(tmp_file, "w") as fp:
        fp.write(file_content)
    msg_output("Prepare run script:\n", file_content)
    run_cmd(global_config["shell"] + " \"" + tmp_file + "\"")
    os.remove(tmp_file)


def build_project():
    global project_config
    global global_config
    pkg_name = project_config["name"] + project_config["version"]
    if "build" in project_config:
        if "command" in project_config["build"]:
            run_cmd_list(project_config["build"]["command"])
        system_type = global_config["system_type"]
        if system_type in project_config["build"]:
            if "command" in project_config["build"][system_type]:
                run_cmd_list(project_config["build"][system_type]["command"])
    succ_output(pkg_name, "build success")


def install_pkg():
    global project_config
    global global_config
    if "install" in project_config:
        if "command" in project_config["install"]:
            run_cmd_list(project_config["install"]["command"])
        system_type = global_config["system_type"]
        if system_type in project_config["install"]:
            if "command" in project_config["install"][system_type]:
                run_cmd_list(project_config["install"][system_type]["command"])
    pkg_name = project_config["name"] + project_config["version"]
    mark_path = os.path.join(global_config["package_install_mark_dir"], pkg_name)
    with open(mark_path, "w") as fp:
        fp.write(json.dumps(project_config))
    succ_output(pkg_name, "install success")


def list_package():
    global project_config
    global global_config
    pkg_list = os.listdir(global_config["package_install_mark_dir"])
    for pkg in pkg_list:
        with open(os.path.join(global_config["package_install_mark_dir"], pkg), "r") as fp:
            pkg_obj = json.loads(fp.read())
            print("Package:",pkg)
            print("Name:",pkg_obj["name"])
            print("Version:",pkg_obj["version"])
            print("Source:",pkg_obj["project_dir"])
            print("--------------------")
    print("Package count:", len(pkg_list))


def remove_package(pkg:str):
    global project_config
    global global_config
    pkg_mark_path = os.path.join(global_config["package_install_mark_dir"], pkg)
    if not os.path.exists(pkg_mark_path):
        print("Package", pkg, "not installed")
        return
    with open(pkg_mark_path, "r") as fp:
        pkg_obj = json.loads(fp.read())
        if "uninstall" in pkg_obj:
            if "command" in pkg_obj["uninstall"]:
                run_cmd_list(pkg_obj["uninstall"]["command"])
            system_type = global_config["system_type"]
            if system_type in pkg_obj["uninstall"]:
                if "command" in pkg_obj["uninstall"][system_type]:
                    run_cmd_list(pkg_obj["uninstall"][system_type]["command"])
        if ask("Delete source (" + pkg_obj["project_dir"] + ")"):
            #shutil.rmtree(pkg_obj["project_dir"],ignore_errors=True)
            del_dir_tree(pkg_obj["project_dir"])
    os.remove(pkg_mark_path)
    print(pkg,"Removed")


def load_pkg_list_config():
    global global_config
    global project_config
    source_file = os.path.join(global_config["pkg_list_config"],"source.json")
    try:
        with open(source_file) as fp:
            global_config["pkg_source"] = json.loads(fp.read())
    except Exception:
        warn_output("Failed to load package configuration file, third-party libraries are not supported， please check the file", source_file)


def main():
    global global_config
    global project_config
    try:
        load_default_config()
        make_install_mark_dir()
        load_pkg_list_config()
        parser = argparse.ArgumentParser(description=version_info)
        parser.add_argument("--list","-l",action="store_true", help="List packages installed")
        parser.add_argument("--remove","-r",type=str,help="Remove a package")
        args = parser.parse_args()
        if args.list:
            list_package()
            return
        if args.remove:
            remove_package(args.remove)
            return


        project_config = {
            "project_dir": global_config["work_dir"],
        }
        load_project_config()
        download_and_build_depend()
        build_project()
        install_pkg()
    except Exception as err:
        error_output(err)
        ask_abort()


if __name__ == "__main__":
    exit(main())